// =======Options START=======
var authConfig = {
    siteName: "Goindex", // 网站名称
    version: "1.1.2", // 程序版本
    theme: "acrou",
    // 强烈推荐使用自己的 client_id 和 client_secret
  
    /**
     * 设置要显示的多个云端硬盘；按格式添加多个
     * [id]: 可以是 团队盘id、子文件夹id、或者"root"（代表个人盘根目录）；
     * [name]: 显示的名称
     * [user]: Basic Auth 的用户名
     * [pass]: Basic Auth 的密码
     * [protect_file_link]: Basic Auth 是否用于保护文件链接，默认值（不设置时）为 false，即不保护文件链接（方便 直链下载/外部播放 等）
     * 每个盘的 Basic Auth 都可以单独设置。Basic Auth 默认保护该盘下所有文件夹/子文件夹路径
     * 【注意】默认不保护文件链接，这样可以方便 直链下载/外部播放;
     *       如果要保护文件链接，需要将 protect_file_link 设置为 true，此时如果要进行外部播放等操作，需要将 host 替换为 user:pass@host 的 形式
     * 不需要 Basic Auth 的盘，保持 user 和 pass 同时为空即可。（直接不设置也可以）
     * 【注意】对于id设置为为子文件夹id的盘将不支持搜索功能（不影响其他盘）。
     */
    default_gd: 0,
    /**
     * 文件列表页面每页显示的数量。【推荐设置值为 100 到 1000 之间】；
     * 如果设置大于1000，会导致请求 drive api 时出错；
     * 如果设置的值过小，会导致文件列表页面滚动条增量加载（分页加载）失效；
     * 此值的另一个作用是，如果目录内文件数大于此设置值（即需要多页展示的），将会对首次列目录结果进行缓存。
     */
    files_list_page_size: 50,
    /**
     * 搜索结果页面每页显示的数量。【推荐设置值为 50 到 1000 之间】；
     * 如果设置大于1000，会导致请求 drive api 时出错；
     * 如果设置的值过小，会导致搜索结果页面滚动条增量加载（分页加载）失效；
     * 此值的大小影响搜索操作的响应速度。
     */
    search_result_list_page_size: 50,
    // 确认有 cors 用途的可以开启
    enable_cors_file_down: false,
    /**
     * 上面的 basic auth 已经包含了盘内全局保护的功能。所以默认不再去认证 .password 文件内的密码;
     * 如果在全局认证的基础上，仍需要给某些目录单独进行 .password 文件内的密码验证的话，将此选项设置为 true;
     * 【注意】如果开启了 .password 文件密码验证，每次列目录都会额外增加查询目录内 .password 文件是否存在的开销。
     */
    enable_password_file_verify: false
  };
  
    var accounts = [
        {
          id: "root",
          name: "Google Drive 1",
          user: "",
          pass: "",
          protect_file_link: false,
          client_id: '',
          client_secret: '',
          refresh_token: ''
        },
        {
          id: "root",
          name: "Google Drive 2",
          user: "",
          pass: "",
          protect_file_link: false,
          client_id: "",
          client_secret: "",
          refresh_token: ""
        }
        // {
        //   id: "root",
        //   name: "Google Drive 3",
        //   pass: "",
        //   client_id: "",
        //   client_secret: "",
        //   refresh_token: ""
        // },
      ]
  
  
  var themeOptions = {
    cdn: "https://cdn.jsdelivr.net/gh/alx-xlx/goindex",
    // 主题版本号
    version: "2.0.8-darkmode-0.1",
    //可选默认系统语言:en/zh-chs/zh-cht
    languages: "en",
    render: {
      /**
       * 是否渲染HEAD.md文件
       * Render HEAD.md file
       */
      head_md: false,
      /**
       * 是否渲染README.md文件
       * Render README.md file
       */
      readme_md: false,
      /**
       * 是否渲染文件/文件夹描述
       * Render file/folder description or not
       */
      desc: false,
    },
    /**
     * 视频播放器选项
     * Video player options
     */
    video: {
      /**
       * 播放器api（不指定则使用默认播放器）
       * Player api(Use default player if not specified)
       */
      api: "",
      autoplay: true,
    },
    /**
     * 音频播放器选项
     * Audio player options
     */
    audio: {},
  };
  // =======Options END=======
  
  /**
   * global functions
   */
  const FUNCS = {
    /**
     * 转换成针对谷歌搜索词法相对安全的搜索关键词
     */
    formatSearchKeyword: function(keyword) {
      let nothing = "";
      let space = " ";
      if (!keyword) return nothing;
      return keyword
        .replace(/(!=)|['"=<>/\\:]/g, nothing)
        .replace(/[,，|(){}]/g, space)
        .trim();
    },
  };
  
  /**
   * global consts
   * @type {{folder_mime_type: string, default_file_fields: string, gd_root_type: {share_drive: number, user_drive: number, sub_folder: number}}}
   */
  const CONSTS = new (class {
    default_file_fields =
      "parents,id,name,mimeType,modifiedTime,createdTime,fileExtension,size";
    gd_root_type = {
      user_drive: 0,
      share_drive: 1,
      sub_folder: 2,
    };
    folder_mime_type = "application/vnd.google-apps.folder";
  })();
  
  // gd instances
  var gds = [];
  
  function html(current_drive_order = 0, model = {}) {
    return `
  <!DOCTYPE html>
  <html>
  <head>
  <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0,maximum-scale=1.0, user-scalable=no"/> <title>${authConfig.siteName}</title> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1"><meta name="description" content="Combining the power of Cloudflare Workers and Google Drive will allow you to index your files on the browser on Cloudflare Workers."><meta name="theme-color" content="#FF3300"><meta name="application-name" content="goindex"><meta name="robots" content="index, follow"><meta name="twitter:card" content="summary"><meta name="twitter:image" content="https://i.imgur.com/rOyuGjA.gif"><meta name="twitter:description" content="Combining the power of Cloudflare Workers and Google Drive will allow you to index your files on the browser on Cloudflare Workers."><meta name="keywords" content="goindex, google, drive, goindex, gdindex, classic, material, workers-script, oauth-consent-screen, google-drive, cloudflare-workers, themes"><meta name="twitter:title" content="Goindex"><meta name="twitter:url" content="https://github.com/alx-xlx/goindex"><link rel="shortcut icon" href="https://i.imgur.com/rOyuGjA.gif"><meta property="og:site_name" content="Goindex"><meta property="og:type" content="website"><meta property="og:image" content="https://i.imgur.com/rOyuGjA.gif"><meta property="og:description" content="Combining the power of Cloudflare Workers and Google Drive will allow you to index your files on the browser on Cloudflare Workers."><meta property="og:title" content="Goindex"><meta property="og:url" content="https://github.com/alx-xlx/goindex"><link rel="apple-touch-icon" href="https://i.imgur.com/rOyuGjA.gif"><link rel="icon" type="image/png" sizes="32x32" href="https://i.imgur.com/rOyuGjA.gif"><meta name="google-site-verification" content="OD_AXMYw-V6ID9xQUb2Wien9Yy8IJSyfBUyejYNB3CU"/><script async src="https://www.googletagmanager.com/gtag/js?id=UA-86099016-6"></script><script>window.dataLayer=window.dataLayer || []; function gtag(){dataLayer.push(arguments);}gtag('js', new Date()); gtag('config', 'UA-86099016-6');</script><script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','GTM-MR47R4M');</script> <style>@import url(${themeOptions.cdn}@${themeOptions.version}/goindex-acrou/dist/style.min.css); </style> <script>window.gdconfig=JSON.parse('${JSON.stringify({version: authConfig.version, themeOptions: themeOptions,})}'); window.themeOptions=JSON.parse('${JSON.stringify(themeOptions)}'); window.gds=JSON.parse('${JSON.stringify(accounts.map((it)=> it.name) )}'); window.MODEL=JSON.parse('${JSON.stringify(model)}'); window.current_drive_order=${current_drive_order}; </script>
  </head>
  <body>
  <noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-MR47R4M"height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
      <div id="app"></div>
      <script src="${themeOptions.cdn}@${
      themeOptions.version
    }/goindex-acrou/dist/app.min.js"></script>
  </body>
  </html>
  `;
  }
  
  addEventListener("fetch", (event) => {
    event.respondWith(handleRequest(event.request));
  });
  
  /**
   * Fetch and log a request
   * @param {Request} request
   */
  async function handleRequest(request) {
    if (gds.length === 0) {
        for (let i = 0; i < this.accounts.length; i++) {
          let authConfig = {
            ...this.authConfig,
            ...this.accounts[i]
          }
          const gd = new googleDrive(authConfig, i);
          gds.push(gd);
        }
      // 这个操作并行，提高效率
      let tasks = [];
      gds.forEach((gd) => {
        tasks.push(gd.initRootType());
      });
      for (let task of tasks) {
        await task;
      }
    }
  
    // 从 path 中提取 drive order
    // 并根据 drive order 获取对应的 gd instance
    let gd;
    let url = new URL(request.url);
    let path = decodeURI(url.pathname);
  
    /**
     * 重定向至起始页
     * @returns {Response}
     */
    function redirectToIndexPage() {
      return new Response("", {
        status: 301,
        headers: { Location: `/${authConfig.default_gd}:/` },
      });
    }
  
    if (path == "/") return redirectToIndexPage();
    if (path.toLowerCase() == "/favicon.ico") {
      // 后面可以找一个 favicon
      return new Response("", { status: 404 });
    }
  
    // 特殊命令格式
    const command_reg = /^\/(?<num>\d+):(?<command>[a-zA-Z0-9]+)(\/.*)?$/g;
    const match = command_reg.exec(path);
    let command;
    if (match) {
      const num = match.groups.num;
      const order = Number(num);
      if (order >= 0 && order < gds.length) {
        gd = gds[order];
      } else {
        return redirectToIndexPage();
      }
      // basic auth
      for (const r = gd.basicAuthResponse(request); r; ) return r;
      command = match.groups.command;
  
      // 搜索
      if (command === "search") {
        if (request.method === "POST") {
          // 搜索结果
          return handleSearch(request, gd);
        } else {
          const params = url.searchParams;
          // 搜索页面
          return new Response(
            html(gd.order, {
              q: params.get("q") || "",
              is_search_page: true,
              root_type: gd.root_type,
            }),
            {
              status: 200,
              headers: { "Content-Type": "text/html; charset=utf-8" },
            }
          );
        }
      } else if (command === "id2path" && request.method === "POST") {
        return handleId2Path(request, gd);
      } else if (command === "view") {
        const params = url.searchParams;
        return gd.view(params.get("url"), request.headers.get("Range"));
      } else if (command !== "down" && request.method === "GET") {
        return new Response(html(gd.order, { root_type: gd.root_type }), {
          status: 200,
          headers: { "Content-Type": "text/html; charset=utf-8" },
        });
      }
    }
    const reg = new RegExp(`^(/\\d+:)${command}/`, "g");
    path = path.replace(reg, (p1, p2) => {
      return p2 + "/";
    });
    // 期望的 path 格式
    const common_reg = /^\/\d+:\/.*$/g;
    try {
      if (!path.match(common_reg)) {
        return redirectToIndexPage();
      }
      let split = path.split("/");
      let order = Number(split[1].slice(0, -1));
      if (order >= 0 && order < gds.length) {
        gd = gds[order];
      } else {
        return redirectToIndexPage();
      }
    } catch (e) {
      return redirectToIndexPage();
    }
  
    // basic auth
    // for (const r = gd.basicAuthResponse(request); r;) return r;
    const basic_auth_res = gd.basicAuthResponse(request);
    path = path.replace(gd.url_path_prefix, "") || "/";
    if (request.method == "POST") {
      return basic_auth_res || apiRequest(request, gd);
    }
  
    let action = url.searchParams.get("a");
  
    if (path.substr(-1) == "/" || action != null) {
      return (
        basic_auth_res ||
        new Response(html(gd.order, { root_type: gd.root_type }), {
          status: 200,
          headers: { "Content-Type": "text/html; charset=utf-8" },
        })
      );
    } else {
      if (
        path
          .split("/")
          .pop()
          .toLowerCase() == ".password"
      ) {
        return basic_auth_res || new Response("", { status: 404 });
      }
      let file = await gd.file(path);
      let range = request.headers.get("Range");
      if (gd.accounts.protect_file_link && basic_auth_res) return basic_auth_res;
      const is_down = !(command && command == "down");
      return gd.down(file.id, range, is_down);
    }
  }
  
  async function apiRequest(request, gd) {
    let url = new URL(request.url);
    let path = url.pathname;
    path = path.replace(gd.url_path_prefix, "") || "/";
  
    let option = { status: 200, headers: { "Access-Control-Allow-Origin": "*" } };
  
    if (path.substr(-1) == "/") {
      let deferred_pass = gd.password(path);
      let body = await request.text();
      body = JSON.parse(body);
      // 这样可以提升首次列目录时的速度。缺点是，如果password验证失败，也依然会产生列目录的开销
      let deferred_list_result = gd.list(
        path,
        body.page_token,
        Number(body.page_index)
      );
  
      // check .password file, if `enable_password_file_verify` is true
      if (authConfig["enable_password_file_verify"]) {
        let password = await gd.password(path);
        // console.log("dir password", password);
        if (password && password.replace("\n", "") !== body.password) {
          let html = `{"error": {"code": 401,"message": "password error."}}`;
          return new Response(html, option);
        }
      }
  
      let list_result = await deferred_list_result;
      return new Response(JSON.stringify(list_result), option);
    } else {
      let file = await gd.file(path);
      let range = request.headers.get("Range");
      return new Response(JSON.stringify(file));
    }
  }
  
  // 处理 search
  async function handleSearch(request, gd) {
    const option = {
      status: 200,
      headers: { "Access-Control-Allow-Origin": "*" },
    };
    let body = await request.text();
    body = JSON.parse(body);
    let search_result = await gd.search(
      body.q || "",
      body.page_token,
      Number(body.page_index)
    );
    return new Response(JSON.stringify(search_result), option);
  }
  
  /**
   * 处理 id2path
   * @param request 需要 id 参数
   * @param gd
   * @returns {Promise<Response>} 【注意】如果从前台接收的id代表的项目不在目标gd盘下，那么response会返回给前台一个空字符串""
   */
  async function handleId2Path(request, gd) {
    const option = {
      status: 200,
      headers: { "Access-Control-Allow-Origin": "*" },
    };
    let body = await request.text();
    body = JSON.parse(body);
    let path = await gd.findPathById(body.id);
    return new Response(path || "", option);
  }
  
  class googleDrive {
    constructor(authConfig, order) {
      // 每个盘对应一个order，对应一个gd实例
      this.order = order;
        this.accounts = accounts[order];
        this.protect_file_link = this.protect_file_link || false;
      this.url_path_prefix = `/${order}:`;
      this.authConfig = authConfig;
      // TODO: 这些缓存的失效刷新策略，后期可以制定一下
      // path id
      this.paths = [];
      // path file
      this.files = [];
      // path pass
      this.passwords = [];
      // id <-> path
      this.id_path_cache = {};
      this.id_path_cache[this.accounts["id"]] = "/";
      this.paths["/"] = this.accounts["id"];
      /*if (this.root['pass'] != "") {
              this.passwords['/'] = this.root['pass'];
          }*/
      // this.init();
    }
  
    /**
     * 初次授权；然后获取 user_drive_real_root_id
     * @returns {Promise<void>}
     */
    async init() {
      await this.accessToken();
      /*await (async () => {
              // 只获取1次
              if (authConfig.user_drive_real_root_id) return;
              const root_obj = await (gds[0] || this).findItemById('root');
              if (root_obj && root_obj.id) {
                  authConfig.user_drive_real_root_id = root_obj.id
              }
          })();*/
      // 等待 user_drive_real_root_id ，只获取1次
      if (authConfig.user_drive_real_root_id) return;
      const root_obj = await (gds[0] || this).findItemById("root");
      if (root_obj && root_obj.id) {
        authConfig.user_drive_real_root_id = root_obj.id;
      }
    }
  
    /**
     * 获取根目录类型，设置到 root_type
     * @returns {Promise<void>}
     */
    async initRootType() {
      const root_id = this.accounts["id"];
      const types = CONSTS.gd_root_type;
      if (root_id === "root" || root_id === authConfig.user_drive_real_root_id) {
        this.root_type = types.user_drive;
      } else {
        const obj = await this.getShareDriveObjById(root_id);
        this.root_type = obj ? types.share_drive : types.sub_folder;
      }
    }
  
    /**
     * Returns a response that requires authorization, or null
     * @param request
     * @returns {Response|null}
     */
    basicAuthResponse(request) {
      const user = this.accounts.user || "",
        pass = this.accounts.pass || "",
        _401 = new Response("Unauthorized", {
          headers: {
            "WWW-Authenticate": `Basic realm="goindex:drive:${this.order}"`,
          },
          status: 401,
        });
      if (user || pass) {
        const auth = request.headers.get("Authorization");
        if (auth) {
          try {
            const [received_user, received_pass] = atob(
              auth.split(" ").pop()
            ).split(":");
            return received_user === user && received_pass === pass ? null : _401;
          } catch (e) {}
        }
      } else return null;
      return _401;
    }
  
    async view(url, range = "", inline = true) {
      let requestOption = await this.requestOption();
      requestOption.headers["Range"] = range;
      let res = await fetch(url, requestOption);
      const { headers } = (res = new Response(res.body, res));
      this.authConfig.enable_cors_file_down &&
        headers.append("Access-Control-Allow-Origin", "*");
      inline === true && headers.set("Content-Disposition", "inline");
      return res;
    }
  
    async down(id, range = "", inline = false) {
      let url = `https://www.googleapis.com/drive/v3/files/${id}?alt=media`;
      let requestOption = await this.requestOption();
      requestOption.headers["Range"] = range;
      let res = await fetch(url, requestOption);
      const { headers } = (res = new Response(res.body, res));
      this.authConfig.enable_cors_file_down &&
        headers.append("Access-Control-Allow-Origin", "*");
      inline === true && headers.set("Content-Disposition", "inline");
      return res;
    }
  
    async file(path) {
      if (typeof this.files[path] == "undefined") {
        this.files[path] = await this._file(path);
      }
      return this.files[path];
    }
  
    async _file(path) {
      let arr = path.split("/");
      let name = arr.pop();
      name = decodeURIComponent(name).replace(/\'/g, "\\'");
      let dir = arr.join("/") + "/";
      // console.log(name, dir);
      let parent = await this.findPathId(dir);
      // console.log(parent);
      let url = "https://www.googleapis.com/drive/v3/files";
      let params = { includeItemsFromAllDrives: true, supportsAllDrives: true };
      params.q = `'${parent}' in parents and name = '${name}' and trashed = false`;
      params.fields =
        "files(id, name, mimeType, size ,createdTime, modifiedTime, iconLink, thumbnailLink)";
      url += "?" + this.enQuery(params);
      let requestOption = await this.requestOption();
      let response = await fetch(url, requestOption);
      let obj = await response.json();
      // console.log(obj);
      return obj.files[0];
    }
  
    // 通过reqeust cache 来缓存
    async list(path, page_token = null, page_index = 0) {
      if (this.path_children_cache == undefined) {
        // { <path> :[ {nextPageToken:'',data:{}}, {nextPageToken:'',data:{}} ...], ...}
        this.path_children_cache = {};
      }
  
      if (
        this.path_children_cache[path] &&
        this.path_children_cache[path][page_index] &&
        this.path_children_cache[path][page_index].data
      ) {
        let child_obj = this.path_children_cache[path][page_index];
        return {
          nextPageToken: child_obj.nextPageToken || null,
          curPageIndex: page_index,
          data: child_obj.data,
        };
      }
  
      let id = await this.findPathId(path);
      let result = await this._ls(id, page_token, page_index);
      let data = result.data;
      // 对有多页的，进行缓存
      if (result.nextPageToken && data.files) {
        if (!Array.isArray(this.path_children_cache[path])) {
          this.path_children_cache[path] = [];
        }
        this.path_children_cache[path][Number(result.curPageIndex)] = {
          nextPageToken: result.nextPageToken,
          data: data,
        };
      }
  
      return result;
    }
  
    async _ls(parent, page_token = null, page_index = 0) {
      // console.log("_ls", parent);
  
      if (parent == undefined) {
        return null;
      }
      let obj;
      let params = { includeItemsFromAllDrives: true, supportsAllDrives: true };
      params.q = `'${parent}' in parents and trashed = false AND name !='.password'`;
      params.orderBy = "folder,name,modifiedTime desc";
      params.fields =
        "nextPageToken, files(name, mimeType, size , modifiedTime, thumbnailLink, description)";
      params.pageSize = this.authConfig.files_list_page_size;
  
      if (page_token) {
        params.pageToken = page_token;
      }
      let url = "https://www.googleapis.com/drive/v3/files";
      url += "?" + this.enQuery(params);
      let requestOption = await this.requestOption();
      let response = await fetch(url, requestOption);
      obj = await response.json();
  
      return {
        nextPageToken: obj.nextPageToken || null,
        curPageIndex: page_index,
        data: obj,
      };
  
      /*do {
              if (pageToken) {
                  params.pageToken = pageToken;
              }
              let url = 'https://www.googleapis.com/drive/v3/files';
              url += '?' + this.enQuery(params);
              let requestOption = await this.requestOption();
              let response = await fetch(url, requestOption);
              obj = await response.json();
              files.push(...obj.files);
              pageToken = obj.nextPageToken;
          } while (pageToken);*/
    }
  
    async password(path) {
      if (this.passwords[path] !== undefined) {
        return this.passwords[path];
      }
  
      // console.log("load", path, ".password", this.passwords[path]);
  
      let file = await this.file(path + ".password");
      if (file == undefined) {
        this.passwords[path] = null;
      } else {
        let url = `https://www.googleapis.com/drive/v3/files/${file.id}?alt=media`;
        let requestOption = await this.requestOption();
        let response = await this.fetch200(url, requestOption);
        this.passwords[path] = await response.text();
      }
  
      return this.passwords[path];
    }
  
    /**
     * 通过 id 获取 share drive 信息
     * @param any_id
     * @returns {Promise<null|{id}|any>} 任何非正常情况都返回 null
     */
    async getShareDriveObjById(any_id) {
      if (!any_id) return null;
      if ("string" !== typeof any_id) return null;
  
      let url = `https://www.googleapis.com/drive/v3/drives/${any_id}`;
      let requestOption = await this.requestOption();
      let res = await fetch(url, requestOption);
      let obj = await res.json();
      if (obj && obj.id) return obj;
  
      return null;
    }
  
    /**
     * 搜索
     * @returns {Promise<{data: null, nextPageToken: null, curPageIndex: number}>}
     */
    async search(origin_keyword, page_token = null, page_index = 0) {
      const types = CONSTS.gd_root_type;
      const is_user_drive = this.root_type === types.user_drive;
      const is_share_drive = this.root_type === types.share_drive;
  
      const empty_result = {
        nextPageToken: null,
        curPageIndex: page_index,
        data: null,
      };
  
      if (!is_user_drive && !is_share_drive) {
        return empty_result;
      }
      let keyword = FUNCS.formatSearchKeyword(origin_keyword);
      if (!keyword) {
        // 关键词为空，返回
        return empty_result;
      }
      let words = keyword.split(/\s+/);
      let name_search_str = `name contains '${words.join(
        "' AND name contains '"
      )}'`;
  
      // corpora 为 user 是个人盘 ，为 drive 是团队盘。配合 driveId
      let params = {};
      if (is_user_drive) {
        params.corpora = "user";
      }
      if (is_share_drive) {
        params.corpora = "drive";
        params.driveId = this.accounts.id;
        // This parameter will only be effective until June 1, 2020. Afterwards shared drive items will be included in the results.
        params.includeItemsFromAllDrives = true;
        params.supportsAllDrives = true;
      }
      if (page_token) {
        params.pageToken = page_token;
      }
      params.q = `trashed = false AND name !='.password' AND (${name_search_str})`;
      params.fields =
        "nextPageToken, files(name, mimeType, size , modifiedTime, thumbnailLink, description)";
      params.pageSize = this.authConfig.search_result_list_page_size;
      // params.orderBy = 'folder,name,modifiedTime desc';
  
      let url = "https://www.googleapis.com/drive/v3/files";
      url += "?" + this.enQuery(params);
      // console.log(params)
      let requestOption = await this.requestOption();
      let response = await fetch(url, requestOption);
      let res_obj = await response.json();
  
      return {
        nextPageToken: res_obj.nextPageToken || null,
        curPageIndex: page_index,
        data: res_obj,
      };
    }
  
    /**
     * 一层一层的向上获取这个文件或文件夹的上级文件夹的 file 对象。注意：会很慢！！！
     * 最多向上寻找到当前 gd 对象的根目录 (root id)
     * 只考虑一条单独的向上链。
     * 【注意】如果此id代表的项目不在目标gd盘下，那么此函数会返回null
     *
     * @param child_id
     * @param contain_myself
     * @returns {Promise<[]>}
     */
    async findParentFilesRecursion(child_id, contain_myself = true) {
      const gd = this;
      const gd_root_id = gd.accounts.id;
      const user_drive_real_root_id = authConfig.user_drive_real_root_id;
      const is_user_drive = gd.root_type === CONSTS.gd_root_type.user_drive;
  
      // 自下向上查询的终点目标id
      const target_top_id = is_user_drive ? user_drive_real_root_id : gd_root_id;
      const fields = CONSTS.default_file_fields;
  
      // [{},{},...]
      const parent_files = [];
      let meet_top = false;
  
      async function addItsFirstParent(file_obj) {
        if (!file_obj) return;
        if (!file_obj.parents) return;
        if (file_obj.parents.length < 1) return;
  
        // ['','',...]
        let p_ids = file_obj.parents;
        if (p_ids && p_ids.length > 0) {
          // its first parent
          const first_p_id = p_ids[0];
          if (first_p_id === target_top_id) {
            meet_top = true;
            return;
          }
          const p_file_obj = await gd.findItemById(first_p_id);
          if (p_file_obj && p_file_obj.id) {
            parent_files.push(p_file_obj);
            await addItsFirstParent(p_file_obj);
          }
        }
      }
  
      const child_obj = await gd.findItemById(child_id);
      if (contain_myself) {
        parent_files.push(child_obj);
      }
      await addItsFirstParent(child_obj);
  
      return meet_top ? parent_files : null;
    }
  
    /**
     * 获取相对于本盘根目录的path
     * @param child_id
     * @returns {Promise<string>} 【注意】如果此id代表的项目不在目标gd盘下，那么此方法会返回空字符串""
     */
    async findPathById(child_id) {
      if (this.id_path_cache[child_id]) {
        return this.id_path_cache[child_id];
      }
  
      const p_files = await this.findParentFilesRecursion(child_id);
      if (!p_files || p_files.length < 1) return "";
  
      let cache = [];
      // 把查出来的每一级的path和id都缓存一下
      p_files.forEach((value, idx) => {
        const is_folder =
          idx === 0 ? p_files[idx].mimeType === CONSTS.folder_mime_type : true;
        let path =
          "/" +
          p_files
            .slice(idx)
            .map((it) => it.name)
            .reverse()
            .join("/");
        if (is_folder) path += "/";
        cache.push({ id: p_files[idx].id, path: path });
      });
  
      cache.forEach((obj) => {
        this.id_path_cache[obj.id] = obj.path;
        this.paths[obj.path] = obj.id;
      });
  
      /*const is_folder = p_files[0].mimeType === CONSTS.folder_mime_type;
          let path = '/' + p_files.map(it => it.name).reverse().join('/');
          if (is_folder) path += '/';*/
  
      return cache[0].path;
    }
  
    // 根据id获取file item
    async findItemById(id) {
      const is_user_drive = this.root_type === CONSTS.gd_root_type.user_drive;
      let url = `https://www.googleapis.com/drive/v3/files/${id}?fields=${
        CONSTS.default_file_fields
      }${is_user_drive ? "" : "&supportsAllDrives=true"}`;
      let requestOption = await this.requestOption();
      let res = await fetch(url, requestOption);
      return await res.json();
    }
  
    async findPathId(path) {
      let c_path = "/";
      let c_id = this.paths[c_path];
  
      let arr = path.trim("/").split("/");
      for (let name of arr) {
        c_path += name + "/";
  
        if (typeof this.paths[c_path] == "undefined") {
          let id = await this._findDirId(c_id, name);
          this.paths[c_path] = id;
        }
  
        c_id = this.paths[c_path];
        if (c_id == undefined || c_id == null) {
          break;
        }
      }
      // console.log(this.paths);
      return this.paths[path];
    }
  
    async _findDirId(parent, name) {
      name = decodeURIComponent(name).replace(/\'/g, "\\'");
  
      // console.log("_findDirId", parent, name);
  
      if (parent == undefined) {
        return null;
      }
  
      let url = "https://www.googleapis.com/drive/v3/files";
      let params = { includeItemsFromAllDrives: true, supportsAllDrives: true };
      params.q = `'${parent}' in parents and mimeType = 'application/vnd.google-apps.folder' and name = '${name}'  and trashed = false`;
      params.fields = "nextPageToken, files(id, name, mimeType)";
      url += "?" + this.enQuery(params);
      let requestOption = await this.requestOption();
      let response = await fetch(url, requestOption);
      let obj = await response.json();
      if (obj.files[0] == undefined) {
        return null;
      }
      return obj.files[0].id;
    }
  
    async accessToken() {
      console.log("accessToken");
      if (
        this.authConfig.expires == undefined ||
        this.authConfig.expires < Date.now()
      ) {
        const obj = await this.fetchAccessToken();
        if (obj.access_token != undefined) {
          this.authConfig.accessToken = obj.access_token;
          this.authConfig.expires = Date.now() + 3500 * 1000;
        }
      }
      return this.authConfig.accessToken;
    }
  
    async fetchAccessToken() {
      console.log("fetchAccessToken");
      const url = "https://www.googleapis.com/oauth2/v4/token";
      const headers = {
        "Content-Type": "application/x-www-form-urlencoded",
      };
      const post_data = {
        client_id: this.authConfig.client_id,
        client_secret: this.authConfig.client_secret,
        refresh_token: this.authConfig.refresh_token,
        grant_type: "refresh_token",
      };
  
      let requestOption = {
        method: "POST",
        headers: headers,
        body: this.enQuery(post_data),
      };
  
      const response = await fetch(url, requestOption);
      return await response.json();
    }
  
    async fetch200(url, requestOption) {
      let response;
      for (let i = 0; i < 3; i++) {
        response = await fetch(url, requestOption);
        console.log(response.status);
        if (response.status != 403) {
          break;
        }
        await this.sleep(800 * (i + 1));
      }
      return response;
    }
  
    async requestOption(headers = {}, method = "GET") {
      const accessToken = await this.accessToken();
      headers["authorization"] = "Bearer " + accessToken;
      return { method: method, headers: headers };
    }
  
    enQuery(data) {
      const ret = [];
      for (let d in data) {
        ret.push(encodeURIComponent(d) + "=" + encodeURIComponent(data[d]));
      }
      return ret.join("&");
    }
  
    sleep(ms) {
      return new Promise(function(resolve, reject) {
        let i = 0;
        setTimeout(function() {
          console.log("sleep" + ms);
          i++;
          if (i >= 2) reject(new Error("i>=2"));
          else resolve(i);
        }, ms);
      });
    }
  }
  
  String.prototype.trim = function(char) {
    if (char) {
      return this.replace(
        new RegExp("^\\" + char + "+|\\" + char + "+$", "g"),
        ""
      );
    }
    return this.replace(/^\s+|\s+$/g, "");
  };
  